﻿using System;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace MemoDb
{
    public abstract class IdMapper
    {
        public TypeStorage TypeStorage { protected get; set; }

        protected IdMapper(PropertyInfo property)
        {
            Property = property;
        }

        private static readonly Func<int, int> IntId = id => id + 1;
        private static readonly Func<long, long> LongId = id => id + 1;
        private static readonly Func<Guid, Guid> GuidId = id => Guid.NewGuid();
        
        public PropertyInfo Property { get; private set; }

        public abstract void SetNewIdIfNeeded(object model);

        public static IdMapper AutoAssign(Type type, string property, object unsavedValue = null)
        {
            return AutoAssign(GetPropertyInfo(type, property));
        }

        private static PropertyInfo GetPropertyInfo(Type type, string property)
        {
            return type.GetProperty(property, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
        }

        public static IdMapper Assigned(Type type, string property, object unsavedValue = null)
        {
            return AutoAssign(GetPropertyInfo(type, property));
        }

        public static IdMapper Assigned(PropertyInfo id, object unsavedValue = null)
        {
            return new AssignedIdMapper(id);
        }

        public static IdMapper AutoAssign(PropertyInfo id, object unsavedValue = null)
        {
            if (id.PropertyType == typeof (int))
                return new IdMapper<int>(id, IntId, unsavedValue ?? default(int));
            if (id.PropertyType == typeof (long))
                return new IdMapper<long>(id, LongId, unsavedValue ?? default(long));
            if (id.PropertyType == typeof (Guid))
                return new IdMapper<Guid>(id, GuidId, unsavedValue ?? Guid.Empty);

            throw new InvalidOperationException(string.Format( "Cannot generate an id for property {0} of type {1}. Property type is {2}. Please, specify a way to get a new id", id.PropertyType, id.DeclaringType, id.PropertyType));
        }

        public object GetId(object source)
        {
            return Property.GetValue(source, null);
        }

        public void SetId(object model, object id)
        {
            Property.SetValue(model, id, null);
        }

        public abstract bool IsNew(object model);
    }

    public class IdMapper<TId> : IdMapper
    {
        private readonly Func<TId, TId> getNewId;

        private readonly object unsavedValue;

        public IdMapper(PropertyInfo id, Func<TId, TId> getNewId, object unsavedValue) : base(id)
        {
            this.getNewId = getNewId;
            this.unsavedValue = unsavedValue;
        }

        private TId currentId;

        [MethodImpl(MethodImplOptions.Synchronized)]
        public override void SetNewIdIfNeeded(object model)
        {
            if(!IsNew(model))
                return;

            currentId = getNewId(currentId);
            SetId(model, currentId);
        }

        public override bool IsNew(object model)
        {
            var id = GetId(model);

            return !((unsavedValue == null && id != null) || (unsavedValue != null && !unsavedValue.Equals(id)));

        }
    }
}